因為 Svelte 還擁有 lifecycle fucntion ,所以我們不必為了模擬生命週期而使用 $effect ,而它們基本上在某些情況中可以說是一樣的東西,所以就只是開發時要想什麼時候要使用 lifecycle 或者 $effect 會比較好寫而已 。
目前 Svelte 有四個 lifecycle function ,分別是 onMount 、 beforeUpdate、afterUpdate、onDestroy ,但在 Svelte 5 後基本上 beforeUpdate、afterUpdate 都可以用 $effect.pre 和 $effect 所代替。
<!-- in Counter.svetle -->
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
let obj = $state({ value: 0 });
let derivedObj = $derived({ value: obj.value * 2 });
let p: HTMLParagraphElement | null = $state(null);
$effect.pre(() => {
console.log(
'\x1b[36m%s\x1b[0m',
`[Pre Effect]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
return () => {
console.log(
'\x1b[36m%s\x1b[0m',
`[Pre Effect Cleanup]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
};
});
$effect(() => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
return () => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1 Cleanup]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
};
});
onMount(() => {
console.log(
'\x1b[33m%s\x1b[0m',
`[onMount]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
onDestroy(() => {
console.log(
'\x1b[31m%s\x1b[0m',
`[onDestroy]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
</script>
<button onclick={() => (obj.value += 1)}> Increment obj.value </button>
<button
onclick={() =>
(obj = {
...obj,
value: obj.value + 1
})}
>
Increment obj.value (immutable)</button
>
<p class="content" bind:this={p}>{obj.value} doubled is {derivedObj.value}</p>
初次掛載的輸出是這樣的:

看得出來 onMount 的執行時機是在 DOM 完成掛載後後,而且看這個輸出順序代表onMount 是會在 $effect 第一次執行完才執行嗎?其實不是
onMount(() => {
console.log(
`\x1b[33m%s\x1b[0m`,
`[onMount]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
$effect(() => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
return () => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1 Cleanup]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
};
});
我們把 onMount 移到 $effect 前就會發現輸出變成

沒錯其實 $effect 的第一次執行跟 onMount 是一樣的東西。可以把 onMount 想成一種只執行一次 $effect ,已知 $effect 就是在 DOM 完成渲染後才會執行,所以這時候純粹就是先執行誰的 effect 的事情而已。
那 onDestroy 也很好理解了,就是我們將 component destroy 時會觸發且也可以把它想像成想成一種只執行一次 $effect 但只有 cleanup 的部分,所以也是先執行 $effect.pre 的 cleanup 後再執行 $effect 的 cleanup 及 onDestroy

看到這裡可能會有疑惑為什麼 onDestroy 或者 $effect 的 cleanup 的 p?.innerText 是有值的?理論上他們執行時機是畫面更新後所以應該會是 undfined 才對,這邊我自己的理解是 destory 是一個特殊的行為,所以他們在 destory 會在 component 真正從 DOM node 被移除之前就先動了。
但 Svelte 提供了一個叫做 tick 的 function ,它會回傳一個 promise 直到任意狀態被改變才會被 resolve 或者沒有狀態的情況下會在下一個 microtask 中 resolve
import { tick } from 'svelte';
onDestroy(() => {
tick().then(() => {
console.log(
'\x1b[31m%s\x1b[0m',
`[onDestroy]\n`,
`p?.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
});
當我們使用 tick 後他就會直到下一個狀態改變後才會 resolve ,然後才會我們 .then 裡面的 console.log 。

那我們再來統整一次 $effect 、 $effect.pre 、 onMount 和 onDestory 的執行順序
初始狀態: $effect.pre → DOM node 掛載完畢 →onMount / $effect
狀態更新:$effect.pre 的cleanup → $effect.pre → DOM node 更新完畢(如果需要)→ $effect 的 cleanup → $effect
移除 component : $effect.pre 的cleanup → $effect 的 cleanup → onDestroy
這幾天一直在說的 Svelte 會自動追蹤依賴然後依賴更新就會更新狀態或執行 effect,那假設有些情況就是我需要在 A 狀態變更下去看 B 狀態的值但我 B 狀態更新時並不想觸發 effect 呢?這時可以使用 untrack ,這個 function 就是告訴 Svelte 說這個依賴不要被自動追蹤。
以這個 Counter 來說我並不在意第一次渲染時 p 被更新時也去觸發 $effect.pre 但我依然想在 $effect.pre 讀取他的值。
import { untrack } from 'svelte';
$effect.pre(() => {
console.log(
'\x1b[36m%s\x1b[0m',
`[Pre Effect]\n`,
`p.innerText: ${untrack(() => p?.innerText)} \n obj.value: ${obj.value}`
);

可以看到初次掛載後就不會有 p 從 undefined 更新後的 $effect.pre 和它的 cleanup 的 console.log 了。
所以甚至我可以讓一個
$effect所有的依賴都被untrack那基本上就跟onMount沒兩樣了。
$effect 很好用但也很容易被濫用,甚至大多數情況下可以不必使用 $effect 也能達成需求。
就像是 React 的
useEffect一樣
很明顯的如果這種情況直接使用 onMount 就好,除非真的是想要在 DOM 掛載前就先執行 effect 才會去使用 $effect.pre
像這種情況可以直接改用 $dervied 就好
// ❌ 不要這樣寫
let count = $state(0)
let double = $state(0)
$effect(()=>{
double = count * 2
})
或許可能會想說有些值很複雜不是一個簡單的值就能表示的 $derived ,那這時可以使用 $derived.by 可以傳入一個 function 最後 return 的值就是要改變的值。
// 這兩個是一樣的作用
let double = $derived( count * 2 )
let double = $derived.by(()=> count * 2 )
這邊直接沿用官方文件的範例,這邊會看到兩個 state : spent 及 left ,功能是不管我是改動 spent 還是 left 另外一個都要隨之更新。
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
$effect(() => {
left = total - spent;
});
$effect(() => {
spent = total - left;
});
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
bind:value簡單解釋就是雙向綁定的語法糖,也就是讓 input 的 value 可以影響 state , state 也可以影響 input 的 value,也可以理解為 React 的 controlled component。
不推薦的原因是不管我是選擇去變更哪一個狀態這兩個 $effect 都會被執行,但我們其實預期的是假設我只變更 spent 那應該只有 $effect 第一個執行然後去計算 left 就好。但實際上會變成
更新 spent → effect 更新了 left → left 更新了所以觸發了 effect 去更新 spent,不能看出可能會有無限迴圈的問題,也剛好這個例子不會發生。
所以文件給出了一個修改的提案:使用 event 更改狀態
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(e) {
spent = +e.target.value;
left = total - spent;
}
function updateLeft(e) {
left = +e.target.value;
spent = total - left;
}
</script>
<label>
<input type="range" value={spent} oninput={updateSpent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" value={left} oninput={updateLeft} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
今天總算把比較常用 rune 都介紹過一輪了,當然目前 rune 不只這只有這些只是只會在某些特殊場合才會需要,就當未來有用到時在特別提出來介紹吧。
https://github.com/toddLiao469469/30days-for-svelte5/tree/main/src/routes/day06